/**
* Vercel MCP Relay Server
*
* This MCP server acts as a relay for the Vercel REST API,
* enabling GitHub Copilot Agents to interact with Vercel services.
*/
import { createMcpHandler } from "@vercel/mcp-adapter";
import { z } from "zod";
const VERCEL_API_BASE = "https://api.vercel.com";
/**
* Make an authenticated request to the Vercel API
*/
async function vercelFetch(
path: string,
token: string,
options: RequestInit = {}
): Promise<Response> {
const url = `${VERCEL_API_BASE}${path}`;
return fetch(url, {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
...options.headers,
},
});
}
/**
* Format error response from Vercel API
*/
async function formatError(response: Response): Promise<string> {
try {
const error = await response.json();
return error.error?.message || JSON.stringify(error);
} catch {
return `HTTP ${response.status}: ${response.statusText}`;
}
}
const handler = createMcpHandler(
(server) => {
// ===== PROJECTS =====
server.tool(
"list_projects",
"List all Vercel projects accessible to the authenticated user. Returns project details including name, framework, and creation date.",
{
teamId: z
.string()
.optional()
.describe("Team ID to filter projects (optional)"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Number of projects to return (1-100, default: 20)"),
},
async ({ teamId, limit }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
if (limit) params.append("limit", String(limit));
const response = await vercelFetch(
`/v9/projects?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"get_project",
"Get detailed information about a specific Vercel project by ID or name.",
{
projectId: z.string().describe("Project ID or name"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v9/projects/${encodeURIComponent(projectId)}?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// ===== DEPLOYMENTS =====
server.tool(
"list_deployments",
"List deployments for a Vercel project or all deployments accessible to the user.",
{
projectId: z
.string()
.optional()
.describe("Filter by project ID (optional)"),
teamId: z.string().optional().describe("Team ID (optional)"),
state: z
.enum([
"BUILDING",
"ERROR",
"INITIALIZING",
"QUEUED",
"READY",
"CANCELED",
])
.optional()
.describe("Filter by deployment state (optional)"),
target: z
.enum(["production", "preview"])
.optional()
.describe("Filter by deployment target (optional)"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Number of deployments to return (1-100, default: 20)"),
},
async ({ projectId, teamId, state, target, limit }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (projectId) params.append("projectId", projectId);
if (teamId) params.append("teamId", teamId);
if (state) params.append("state", state);
if (target) params.append("target", target);
if (limit) params.append("limit", String(limit));
const response = await vercelFetch(
`/v6/deployments?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"get_deployment",
"Get detailed information about a specific deployment by ID or URL.",
{
deploymentId: z.string().describe("Deployment ID or URL"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ deploymentId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v13/deployments/${encodeURIComponent(deploymentId)}?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"get_deployment_events",
"Get build logs and events for a specific deployment.",
{
deploymentId: z.string().describe("Deployment ID"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ deploymentId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v3/deployments/${encodeURIComponent(deploymentId)}/events?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"cancel_deployment",
"Cancel a deployment that is currently building.",
{
deploymentId: z.string().describe("Deployment ID to cancel"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ deploymentId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v12/deployments/${encodeURIComponent(deploymentId)}/cancel?${params.toString()}`,
token,
{ method: "PATCH" }
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// ===== ENVIRONMENT VARIABLES =====
server.tool(
"list_environment_variables",
"List all environment variables for a Vercel project.",
{
projectId: z.string().describe("Project ID or name"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v9/projects/${encodeURIComponent(projectId)}/env?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"create_environment_variable",
"Create a new environment variable for a Vercel project.",
{
projectId: z.string().describe("Project ID or name"),
key: z.string().describe("Environment variable name"),
value: z.string().describe("Environment variable value"),
target: z
.array(z.enum(["production", "preview", "development"]))
.describe(
"Deployment targets where this variable should be available"
),
type: z
.enum(["plain", "encrypted", "secret", "sensitive"])
.default("encrypted")
.describe("Type of environment variable (default: encrypted)"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, key, value, target, type, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v10/projects/${encodeURIComponent(projectId)}/env?${params.toString()}`,
token,
{
method: "POST",
body: JSON.stringify({ key, value, target, type }),
}
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"delete_environment_variable",
"Delete an environment variable from a Vercel project.",
{
projectId: z.string().describe("Project ID or name"),
envId: z.string().describe("Environment variable ID"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, envId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v9/projects/${encodeURIComponent(projectId)}/env/${encodeURIComponent(envId)}?${params.toString()}`,
token,
{ method: "DELETE" }
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
return {
content: [
{ type: "text", text: "Environment variable deleted successfully" },
],
};
}
);
// ===== DOMAINS =====
server.tool(
"list_project_domains",
"List all domains configured for a Vercel project.",
{
projectId: z.string().describe("Project ID or name"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v9/projects/${encodeURIComponent(projectId)}/domains?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"add_project_domain",
"Add a custom domain to a Vercel project.",
{
projectId: z.string().describe("Project ID or name"),
domain: z.string().describe("Domain name to add (e.g., example.com)"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, domain, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v10/projects/${encodeURIComponent(projectId)}/domains?${params.toString()}`,
token,
{
method: "POST",
body: JSON.stringify({ name: domain }),
}
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"remove_project_domain",
"Remove a domain from a Vercel project.",
{
projectId: z.string().describe("Project ID or name"),
domain: z.string().describe("Domain name to remove"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, domain, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}?${params.toString()}`,
token,
{ method: "DELETE" }
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
return {
content: [{ type: "text", text: "Domain removed successfully" }],
};
}
);
server.tool(
"verify_project_domain",
"Verify a domain's DNS configuration for a Vercel project.",
{
projectId: z.string().describe("Project ID or name"),
domain: z.string().describe("Domain name to verify"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, domain, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v9/projects/${encodeURIComponent(projectId)}/domains/${encodeURIComponent(domain)}/verify?${params.toString()}`,
token,
{ method: "POST" }
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// ===== TEAMS =====
server.tool(
"list_teams",
"List all teams the authenticated user is a member of.",
{},
async () => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const response = await vercelFetch("/v2/teams", token);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
server.tool(
"get_team",
"Get detailed information about a specific team.",
{
teamId: z.string().describe("Team ID or slug"),
},
async ({ teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const response = await vercelFetch(
`/v2/teams/${encodeURIComponent(teamId)}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// ===== USER =====
server.tool(
"get_user",
"Get information about the authenticated user.",
{},
async () => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const response = await vercelFetch("/v2/user", token);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// ===== ALIASES =====
server.tool(
"list_aliases",
"List all aliases (custom URLs) for a deployment or project.",
{
deploymentId: z
.string()
.optional()
.describe("Filter by deployment ID (optional)"),
projectId: z
.string()
.optional()
.describe("Filter by project ID (optional)"),
teamId: z.string().optional().describe("Team ID (optional)"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(20)
.describe("Number of aliases to return (1-100, default: 20)"),
},
async ({ deploymentId, projectId, teamId, limit }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (deploymentId) params.append("deploymentId", deploymentId);
if (projectId) params.append("projectId", projectId);
if (teamId) params.append("teamId", teamId);
if (limit) params.append("limit", String(limit));
const response = await vercelFetch(
`/v4/aliases?${params.toString()}`,
token
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// ===== PROMOTION (Production Aliasing) =====
server.tool(
"promote_deployment",
"Promote a deployment to production by aliasing it to the production domain.",
{
projectId: z.string().describe("Project ID or name"),
deploymentId: z.string().describe("Deployment ID to promote"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, deploymentId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v10/projects/${encodeURIComponent(projectId)}/promote/${encodeURIComponent(deploymentId)}?${params.toString()}`,
token,
{ method: "POST" }
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
// ===== ROLLBACK =====
server.tool(
"rollback_deployment",
"Rollback a project to a previous deployment.",
{
projectId: z.string().describe("Project ID or name"),
deploymentId: z.string().describe("Deployment ID to rollback to"),
teamId: z.string().optional().describe("Team ID (optional)"),
},
async ({ projectId, deploymentId, teamId }) => {
const token = process.env.VERCEL_TOKEN;
if (!token) {
return {
content: [
{
type: "text",
text: "Error: VERCEL_TOKEN environment variable is not set",
},
],
};
}
const params = new URLSearchParams();
if (teamId) params.append("teamId", teamId);
const response = await vercelFetch(
`/v9/projects/${encodeURIComponent(projectId)}/rollback/${encodeURIComponent(deploymentId)}?${params.toString()}`,
token,
{ method: "POST" }
);
if (!response.ok) {
return {
content: [
{ type: "text", text: `Error: ${await formatError(response)}` },
],
};
}
const data = await response.json();
return {
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
};
}
);
},
{},
{
basePath: "/api",
maxDuration: 60,
verboseLogs: process.env.NODE_ENV === "development",
}
);
export { handler as GET, handler as POST, handler as DELETE };